package com.quantum.remoting
{

	import com.quantum.util.Assert;
	import com.quantum.util.ArrayUtils;
	import com.quantum.util.StringUtils;
	import com.quantum.logging.LogFactory;

	import flash.events.TimerEvent;
	import flash.utils.Timer;

	import mx.logging.ILogger;
	import mx.messaging.events.ChannelFaultEvent;
	import mx.rpc.AbstractOperation;
	import mx.rpc.AsyncToken;
	import mx.rpc.Fault;
	import mx.rpc.IResponder;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;

	/**
	 * Invokes operation representing an individual method on a service. Also provides the
	 * utility of retrying a failed request incase the failure was due to a network failure.
	 */
	internal class OperationInvocation implements IResponder
	{
		private static var log:ILogger=LogFactory.getLogger(OperationInvocation);

		private static const DEFAULT_RETRY_COUNT:int=10;
		private static const DEFAULT_RETRY_PERIOD:Number=500; //In ms.
		// Error code and descriptions
		private static const NETCONNECTION_CALL_FAILED:String="NetConnection.Call.Failed";
		private static const HTTP_FAILED:String="HTTP: Failed";

		/**
		 * Number of times to retry failed operations.
		 */
		public var retryCount:int=DEFAULT_RETRY_COUNT;

		/**
		 * Time between detection of failure and next retry of an operation specified in ms.
		 */
		public var retryPeriod:Number=DEFAULT_RETRY_PERIOD;

		private var _timer:Timer;
		private var _currentRetryCount:int=0;
		private var _retryTokens:Array;

		/**
		 * Will invoke the operation and return the result or fault to the supplied callback.
		 *
		 * @param operation
		 *    Operation of service to be invoked.
		 *
		 * @param arguments
		 *    Arguments of the operation.
		 *
		 * @param callback
		 *    The response of invocation is provided to the callback.
		 *
		 *    <code>function(event:Event, callContext:Object)</code>
		 *    Expect event to be of type ResultEvent or FaultEvent.
		 *
		 * @param callContext
		 *   Free form context which will be returned to the callback as is.
		 */
		public function invoke(operation:AbstractOperation, arguments:Array, callback:Function, callContext:Object):void
		{
			operation.arguments=arguments;
			var token:AsyncToken=operation.send();
			token.operation=operation;
			token.addResponder(this);
			token.callback=callback;
			token.callContext=callContext;

		}

		/**
		 * @inheritDoc
		 */
		public function result(data:Object):void
		{
			var event:ResultEvent=data as ResultEvent;
			Assert.assert(event != null, "Expecting data to be a ResultEvent.");
			if (event == null)
			{
				return;
			}
			// Since this is a successful response replay all requests
			replayFailedOperations();
			var token:AsyncToken=event.token;
			resultResponseForRequest(event, token);
		}

		/**
		 * @inheritDoc
		 */
		public function fault(info:Object):void
		{
			var event:FaultEvent=info as FaultEvent;
			Assert.assert(event != null, "Expecting info to be a FaultEvent.");
			if (event == null)
			{
				return;
			}
			var token:AsyncToken=event.token;
			if (retryOnFault(event, token))
			{
				return;
			}
			faultResponseForRequest(event, token);
		}

		/**
		 * Provides a result response to the contained BaseProxy.
		 */
		private function resultResponseForRequest(event:ResultEvent, token:AsyncToken):void
		{
			var callback:Function=token.callback;
			var callContext:Object=token.callContext;
			if (callback != null)
			{
				callback(event, callContext);
			}
		}

		/**
		 * Provides a fault response to the contained BaseProxy.
		 */
		private function faultResponseForRequest(event:FaultEvent, token:AsyncToken):void
		{
			var callback:Function=token.callback;
			var callContext:Object=token.callContext;
			if (callback != null)
			{
				callback(event, callContext);
			}
		}

		/**
		 * Will replay the operation contained in the token on the tick of a timer.
		 */
		private function replayOperationOnTimer(token:AsyncToken):void
		{
			if (_timer == null)
			{
				_timer=new Timer(retryPeriod, 1);
				_timer.addEventListener(TimerEvent.TIMER, onTimerEvent);
			}
			if (_retryTokens == null)
			{
				_retryTokens=[];
			}
			// As an extra precaution remove responder from this token.
			ArrayUtils.removeItemFromArray(this, token.responders);
			// Adding to back of the retry token queue so that it can be used to replay
			log.debug("Adding token to retry list for a replay of the operation.");
			_retryTokens.push(token);
			// If the timer is already running from a previous replay attempt then just queue
			// up the token for replay
			if (!_timer.running)
			{
				_timer.reset();
				_timer.start();
			}
		}

		/**
		 * Picks the first token from the tokens list and replays the contained operation.
		 */
		private function onTimerEvent(event:TimerEvent):void
		{
			// stop the timer to prevent any more events until the next fault is detected.
			_timer.stop();
			// Nothing to retry
			if (_retryTokens.length < 1)
			{
				return;
			}
			// Pick up the first token in the retry list. It really does not matter which
			// request is replayed so no single replay token is maintained.
			var token:AsyncToken=_retryTokens.shift();
			// increment retry counter
			++_currentRetryCount;
			replayOperation(token);
		}

		/**
		 * Replays all the failed operations.
		 */
		private function replayFailedOperations():void
		{
			if (_currentRetryCount > 0)
			{
				log.info("Seems like the client recovered from a network failure.");
			}
			_currentRetryCount=0;
			clearTimer();
			if (ArrayUtils.isEmpty(_retryTokens))
			{
				return;
			}
			// Replay all request in a tight loop.
			for each (var token:AsyncToken in _retryTokens)
			{
				replayOperation(token);
			}
			_retryTokens=null;
		}

		/**
		 * Replays the operation contained in the supplied token.
		 */
		private function replayOperation(token:AsyncToken):void
		{
			var operation:AbstractOperation=token.operation;
			var callback:Function=token.callback;
			var callContext:Object=token.callContext;
			invoke(operation, operation.arguments as Array, callback, callContext);
		}

		/**
		 * Delivers a fault response to all the failed operations. Since this  method should
		 * be called only on failure to reestablish connection it will not reset
		 * _currentRetryCount. This way all successive replayable faults will be immediately
		 * returned as errors until the next succesful result or non-replayable fault.
		 */
		private function faultResponseToFailedOperations(event:FaultEvent):void
		{
			clearTimer();
			if (ArrayUtils.isEmpty(_retryTokens))
			{
				return;
			}
			// Replay all request in a tight loop.
			for each (var token:AsyncToken in _retryTokens)
			{
				faultResponseForRequest(event, token);
			}
			_retryTokens=null;
		}

		/**
		 * Will retry the operation if event indicates a failure warranting a retry. In case
		 * event does not provide such an indication will replay all the operations that
		 * are currently saved as failures.
		 *
		 * Note : Will only retry an operation on a potential network failure.
		 *
		 * @return
		 *    Returns true if the result should be hidden from the caller.
		 */
		private function retryOnFault(event:FaultEvent, token:AsyncToken):Boolean
		{
			if (event == null || event.fault == null)
			{
				return false;
			}
			var retriableFailure:Boolean=isPotentialNetworkFailure(event.fault);
			if (retriableFailure && event.fault != null)
			{
				log.error("A failure which could be fixed by a retry is detected - " + event.fault.message);
			}
			else
			{
				// Is not a network failure so replay all the failed requests as this could mean
				// that the network is back again.
				replayFailedOperations();
				return false;
			}
			if (_currentRetryCount < this.retryCount)
			{
				replayOperationOnTimer(token);
				return true;
			}
			log.error("Unable to recover from failure.");
			// Is a network failure and max retries have been attempted so return errors.
			// Since this is the same error ok to reuse the FaultEvent.
			faultResponseToFailedOperations(event);
			return false;
		}

		/**
		 * Checks if fault indicates a possible network failure.
		 */
		private function isPotentialNetworkFailure(fault:Fault):Boolean
		{
			var channelFaultEv:ChannelFaultEvent=fault.rootCause as ChannelFaultEvent;
			if (channelFaultEv == null || channelFaultEv.rootCause == null)
			{
				return false;
			}
			// This rootcause implies that the server is unreachable because of one of the
			// following issues :
			// - Network failure
			// - Server shutdown
			// Note : The description in case the messagebroker goes down would contain
			// an error code like HTTP 400.
			return channelFaultEv.rootCause.code == NETCONNECTION_CALL_FAILED && channelFaultEv.rootCause.description == HTTP_FAILED;
		}

		/**
		 * Stop timer, remove event listeners and null out reference.
		 */
		private function clearTimer():void
		{
			if (_timer == null)
			{
				return;
			}
			_timer.stop();
			_timer.removeEventListener(TimerEvent.TIMER, onTimerEvent);
			_timer=null;
		}
	}
}